/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is Forte for Java, Community Edition. The Initial
* Developer of the Original Code is Sun Microsystems, Inc. Portions
* Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
*/
package org.openide.util;
import java.util.Map;
import java.util.Locale;
import java.util.Date;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.HashMap;
import java.text.Format;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.ParseException;
import java.text.MessageFormat;
/**
* The message formatter, which uses map's keys in place of numbers.
* This class extends functionality of MessageFormat by allowing the user to
* specify strings in place of MessageFormatter's numbers. It then uses given
* map to translate keys to values.
*
* You will usually use this formatter as follows:
* <code>MapFormat.format("Hello {name}", map);</code>
*
* Notes: if map does not contain value for key specified, it substitutes
* the value by <code>"null"</code> word to qualify that something goes wrong.
*
* @author Slavek Psenicka
* @version 1.0, March 11. 1999
*/
public class MapFormat extends Format {
private static final int BUFSIZE = 255;
/** Locale region settings used for number and date formatting */
private Locale locale = Locale.getDefault();
/** Left delimiter */
private String ldel = "{"; // NOI18N
/** Right delimiter */
private String rdel = "}"; // NOI18N
/** Used formatting map */
private Map argmap;
/** Offsets to {} expressions */
private int[] offsets;
/** Keys enclosed by {} brackets */
private String[] arguments;
/** Max used offset */
private int maxOffset;
/** Should be thrown an exception if key was not found? */
private boolean throwex = false;
/** Exactly match brackets? */
private boolean exactmatch = true;
/** Array with to-be-skipped blocks */
private RangeList skipped;
static final long serialVersionUID =-7695811542873819435L;
/**
* Constructor.
* For common work use <code>format(pattern, arguments) </code>.
* @param pattern String to be parsed.
*/
public MapFormat(Map arguments) {
super();
setMap(arguments);
}
/**
* Designated method. It gets the string, initializes HashFormat object
* and returns converted string. It scans <code>pattern</code>
* for {} brackets, then parses enclosed string and replaces it
* with argument's <code>get()</code> value.
* @param pattern String to be parsed.
* @param arguments Map with key-value pairs to replace.
* @return Formatted string
*/
public static String format(String pattern, Map arguments) {
MapFormat temp = new MapFormat(arguments);
return temp.format(pattern);
}
/**
* Search for comments and quotation marks.
* Prepares internal structures.
* @param pattern String to be parsed.
* @param lmark Left mark of to-be-skipped block.
* @param rmark Right mark of to-be-skipped block or null if does not exist (// comment).
*/
private void process(String pattern, String lmark, String rmark)
{
int idx = 0;
while (true) {
int ridx = -1, lidx = pattern.indexOf(lmark,idx);
if (lidx >= 0) {
if (rmark != null) {
ridx = pattern.indexOf(rmark,lidx + lmark.length());
} else ridx = pattern.length();
} else break;
if (ridx >= 0) {
skipped.put(new Range(lidx, ridx-lidx));
if (rmark != null) idx = ridx+rmark.length();
else break;
} else break;
}
}
/** Returns the value for given key. Subclass may define its own beahvior of
* this method. For example, if key is not defined, subclass can return <not defined>
* string.
*
* @param key Key.
* @return Value for this key.
*/
protected Object processKey(String key) {
try {
return argmap.get(key);
} catch (Exception exc) {
return key;
}
}
/**
* Scans the pattern and prepares internal variables.
* @param newPattern String to be parsed.
* @exception IllegalArgumentException if number of arguments exceeds BUFSIZE or
* parser found unmatched brackets (this exception should be switched off
* using setExactMatch(false)).
*/
public String processPattern(String newPattern)
throws IllegalArgumentException
{
int idx = 0, offnum = -1;
StringBuffer outpat = new StringBuffer();
offsets = new int[BUFSIZE];
arguments = new String[BUFSIZE];
maxOffset = -1;
skipped = new RangeList();
process(newPattern, "\"", "\""); // NOI18N
while (true) {
int ridx = -1, lidx = newPattern.indexOf(ldel,idx);
Range ran = skipped.getRangeContainingOffset(lidx);
if (ran != null) {
outpat.append(newPattern.substring(idx, ran.getEnd()));
idx = ran.getEnd(); continue;
}
if (lidx >= 0) {
ridx = newPattern.indexOf(rdel,lidx + ldel.length());
} else break;
if (++offnum >= BUFSIZE) throw new IllegalArgumentException(NbBundle.getBundle(MapFormat.class).getString("MSG_TooManyArguments"));
if (ridx < 0) {
if (exactmatch) throw new IllegalArgumentException(NbBundle.getBundle(MapFormat.class).getString("MSG_UnmatchedBraces") +" "+lidx);
else break;
}
outpat.append(newPattern.substring(idx, lidx));
offsets[offnum] = outpat.length();
arguments[offnum] = newPattern.substring(lidx + ldel.length(), ridx);
idx = ridx + rdel.length();
maxOffset++;
}
outpat.append(newPattern.substring(idx));
return outpat.toString();
}
/**
* Formats object.
* @param obj Object to be formatted into string
* @return Formatted object
*/
private String formatObject(Object obj)
{
if (obj == null) return null;
if (obj instanceof Number) {
return NumberFormat.getInstance(locale).format(obj); // fix
} else if (obj instanceof Date) {
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale).format(obj);//fix
} else if (obj instanceof String) return (String)obj;
return obj.toString();
}
/**
* Formats the parsed string by inserting table's values.
* @param table Map with key-value pairs to replace.
* @param result Buffer to be used for result.
* @return Formatted string
*/
public StringBuffer format(Object pat, StringBuffer result, FieldPosition fpos)
{
String pattern = processPattern((String)pat);
int lastOffset = 0;
for (int i = 0; i <= maxOffset; ++i) {
int offidx = offsets[i];
result.append(pattern.substring(lastOffset, offsets[i]));
lastOffset = offidx;
String key = arguments[i];
String obj = formatObject(processKey(key));
if (obj == null) {
if (throwex) throw new IllegalArgumentException(MessageFormat.format(NbBundle.getBundle(MapFormat.class).getString("MSG_FMT_ObjectForKey"), new Object [] {new Integer (key)}));
else obj = ldel+key+rdel;
}
result.append(obj);
}
result.append(pattern.substring(lastOffset, pattern.length()));
return result;
}
/**
* Parses the string. Does not yet handle recursion (where
* the substituted strings contain %n references.)
*/
public Object parseObject (String text, ParsePosition status) {
return parse(text);
}
/**
* Parses the string. Does not yet handle recursion (where
* the substituted strings contain {n} references.)
* @return New format.
*/
public String parse(String source)
{
StringBuffer sbuf = new StringBuffer(source);
Iterator key_it = argmap.keySet().iterator();
skipped = new RangeList();
process(source, "\"", "\""); // NOI18N
while (key_it.hasNext()) {
String it_key = (String)key_it.next();
String it_obj = formatObject(argmap.get(it_key));
int it_idx = -1;
do {
it_idx = sbuf.toString().indexOf(it_obj, ++it_idx);
if (it_idx >= 0 && !skipped.containsOffset(it_idx)) {
sbuf.replace(it_idx, it_idx+it_obj.length(), ldel+it_key+rdel);
skipped = new RangeList();
process(sbuf.toString(), "\"", "\""); // NOI18N
}
} while (it_idx != -1);
}
return sbuf.toString();
}
/** Should formatter throw exception if object for key was not found?
* If given map does not contain object for key specified, it could
* throw an exception. Returns true if throws. If not, key is left unchanged.
*/
public boolean willThrowExceptionIfKeyWasNotFound()
{
return throwex;
}
/** Should formatter throw exception if object for key was not found?
* If given map does not contain object for key specified, it could
* throw an exception. If does not throw, key is left unchanged.
* @param flag If true, formatter throws IllegalArgumentException.
*/
public void setThrowExceptionIfKeyWasNotFound(boolean flag)
{
throwex = flag;
}
/** Do you require both brackets in expression?
* If not, use setExactMatch(false) and formatter will ignore missing right
* bracket. Advanced feature.
*/
public boolean isExactMatch()
{
return exactmatch;
}
/** Do you require both brackets in expression?
* If not, use setExactMatch(false) and formatter will ignore missing right
* bracket. Advanced feature.
* @param flag If true, formatter will ignore missing right bracket (default = false)
*/
public void setExactMatch(boolean flag)
{
exactmatch = flag;
}
/** Returns string used as left brace */
public String getLeftBrace()
{
return ldel;
}
/** Sets string used as left brace
* @param delimiter Left brace.
*/
public void setLeftBrace(String delimiter)
{
ldel = delimiter;
}
/** Returns string used as right brace */
public String getRightBrace()
{
return rdel;
}
/** Sets string used as right brace
* @param delimiter Right brace.
*/
public void setRightBrace(String delimiter)
{
rdel = delimiter;
}
/** Returns argument map */
public Map getMap()
{
return argmap;
}
/** Sets argument map
* This map should contain key-value pairs with key values used in
* formatted string expression. If value for key was not found, formatter leave
* key unchanged (except if you've set setThrowExceptionIfKeyWasNotFound(true),
* then it fires IllegalArgumentException.
*
* @param delimiter Right brace.
*/
public void setMap(Map map)
{
argmap = map;
}
/**
* Range of expression in string.
* Used internally to store information about quotation marks and comments
* in formatted string.
*
* @author Slavek Psenicka
* @version 1.0, March 11. 1999
*/
class Range extends Object
{
/** Offset of expression */
private int offset;
/** Length of expression */
private int length;
/** Constructor */
public Range(int off, int len)
{
offset = off;
length = len;
}
/** Returns offset */
public int getOffset()
{
return offset;
}
/** Returns length of expression */
public int getLength()
{
return length;
}
/** Returns final position of expression */
public int getEnd()
{
return offset+length;
}
public String toString()
{
return "("+offset+", "+length+")"; // NOI18N
}
}
/**
* List of ranges.
* Used internally to store information about quotation marks and comments
* in formatted string.
*
* @author Slavek Psenicka
* @version 1.0, March 11. 1999
*/
class RangeList
{
/** Map with Ranges
* @associates Range*/
private HashMap hmap;
/** Constructor */
public RangeList()
{
hmap = new HashMap();
}
/** Returns true if offset is enclosed by any Range object in list */
public boolean containsOffset(int offset)
{
return (getRangeContainingOffset(offset) != null);
}
/** Returns enclosing Range object in list for given offset */
public Range getRangeContainingOffset(int offset)
{
if (hmap.size() == 0) return null;
int offit = offset;
while (offit-- >= 0) {
Integer off = new Integer(offit);
if (hmap.containsKey(off)) {
Range ran = (Range)hmap.get(off);
if (ran.getEnd() - offset > 0) return ran;
}
}
return null;
}
/** Puts new range into list */
public void put(Range range)
{
hmap.put(new Integer(range.getOffset()), range);
}
public String toString()
{
return hmap.toString();
}
}
}
/*
* Log
* 12 Gandalf 1.11 1/12/00 Pavel Buzek I18N
* 11 Gandalf 1.10 1/10/00 Radko Najman fixed bug #4368
* 10 Gandalf 1.9 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun
* Microsystems Copyright in File Comment
* 9 Gandalf 1.8 8/17/99 Ian Formanek Generated serial version
* UID
* 8 Gandalf 1.7 7/27/99 Slavek Psenicka available argument count
* increased (now 255)
* 7 Gandalf 1.6 7/1/99 Martin Ryzl processKey() method added
*
* 6 Gandalf 1.5 6/8/99 Ian Formanek ---- Package Change To
* org.openide ----
* 5 Gandalf 1.4 4/6/99 Petr Hamernik substitution works in the
* comments too
* 4 Gandalf 1.3 4/1/99 Slavek Psenicka Oprava parse: pri
* opakovanem vyskytu formatovaneho tokenu se tento rozpoznal pouze jednou.
* 3 Gandalf 1.2 4/1/99 Slavek Psenicka Vynechani komentaru a
* uvozenych textu z formatovani i parsovani.
* 2 Gandalf 1.1 4/1/99 Slavek Psenicka Zmena prace; inicializace
* s mapou a formatovani vice stringu. Doplneni moznosti nebazirovat na
* ukonceni tokenu.
* 1 Gandalf 1.0 3/23/99 Slavek Psenicka
* $
*/